/*
 * Copyright � 2011 Arizona State University
 *
 * This file is part of ProMDATE.
 *
 * ProMDATE is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * ProMDATE is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with ProMDATE.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <fstream>
#include <sys/stat.h>
#include "globalStatic.h"
#include "HTTPConnection.h"
#include "WebSocketConnection.h"

HTTPConnection::HTTPConnection(int sockfd) :
	HighLevelConnection(sockfd) {
	;
}

HTTPConnection::~HTTPConnection() {
	;
}

int HTTPConnection::sendResponse(unsigned short flags, const char* buffer, unsigned int bufferSize, unsigned int length) {
	switch (flags & HTTP_STATUS_MASK) {
	case HTTP_OK: {
		stringstream headstream;
		headstream << "HTTP/1.1 200 OK\r\n";
		headstream << "Content-length: " << length << "\r\n";

		if (flags & HTTP_MIME_MASK)
			headstream << "Content-type: " << getMIME(flags) << "\r\n";

		if (flags & HTTP_ENCOD_MASK)
			headstream << "Content-encoding: " << getEncod(flags) << "\r\n";

		headstream << "\r\n";

		sendTCP(headstream.str().c_str(), headstream.str().length());
		return sendTCP(buffer, bufferSize);
	}
	case HTTP_NOT_FOUND: {
		string content = "HTTP/1.1 404 Not Found";
		stringstream headstream;
		headstream << "HTTP/1.1 404 Not Found\r\nContent-Length: " << content.length() << "\r\n"
				<< "Content-type: text/plain\r\n\r\n";
		sendTCP(headstream.str().c_str(), headstream.str().length());
		return sendTCP(content.c_str(), content.length());
	}
	case HTTP_UNAUTH: {
		string content = "HTTP/1.1 401 Unauthorized";
		stringstream headstream;
		headstream << "HTTP/1.1 401 Unauthorized\r\n";
		string realm(buffer, bufferSize);
		headstream << "WWW-Authenticate: Basic realm=\"" << realm << "\"\r\n" << "Content-Length: " << content.length()
				<< "\r\n" << "Content-type: text/plain\r\n\r\n";
		sendTCP(headstream.str().c_str(), headstream.str().length());
		return sendTCP(content.c_str(), content.length());
	}
	case HTTP_MOVED: {
		string location(buffer, bufferSize);
		stringstream headstream;
		headstream << "HTTP/1.1 301 Moved\r\n";
		headstream << "Location: " << location << "\r\nContent-Length: 0\r\n\r\n";
		return sendTCP(headstream.str().c_str(), headstream.str().length());
	}
	default: {
		return sendTCP(buffer, bufferSize);
	}
	}
}

bool HTTPConnection::acceptsType(string type) {
	if (accepts.find(type) == string::npos)
		return false;
	return true;
}

bool HTTPConnection::passesAuth(string userPass) {
	int pos = auth.find("Basic ");
	if (pos == string::npos)
		return false;
	string mencoded = auth.substr(pos + 6);

	char table[] = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
			'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
			'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8',
			'9', '+', '/', '=' };

	string mdecoded = "";
	unsigned int temp = 0;
	int numsIn = 0;

	for (int i = 0; i < mencoded.length(); i++) {
		unsigned short j;
		// Reference character against table for base64 value
		for (j = 0; j < 65; j++)
			if (mencoded[i] == table[j])
				break;

		// If not at end (marked by '='), shift temp to left and
		// add new base64 value
		if (j != 64) {
			temp = (temp << 6) + j;
			numsIn++;
		}

		// If four 6-bit base64 values added to temp, dump to
		// three ASCII chars, otherwise check for end indicators.
		// Break loop if end found by '='
		if (numsIn == 4) {
			numsIn = 0;
			for (int k = 2; k > -1; k--)
				mdecoded += (char) ((temp >> k * 8) & 0xff);
		} else if (i == mencoded.length() - 1 || j == 64) {
			if (numsIn == 2)
				temp = temp >> 4;
			else
				temp = temp >> 2;
			for (int k = numsIn - 2; k > -1; k--)
				mdecoded += (char) ((temp >> k * 8) & 0xff);
			break;
		}
	}

	if (mdecoded.compare(userPass) == 0)
		return true;
	return false;
}

void HTTPConnection::listener() {
	char* section;
	int retVal;
	ByteString bytes;

	while (true) {
		retVal = recvTCP(section);

		if (retVal < 1) {
			if (retVal == 0 || !(errno == EAGAIN || errno == EWOULDBLOCK) || connection.find("keep-alive")
					== string::npos) {
				closeSocket();
				break;
			} else
				continue;
		}

		bytes.addSection(section, retVal);
		int pos = bytes.findSequence("\r\n\r\n", 4);
		if (pos == -1)
			continue;

		if (!processHeader(bytes)) {
			string headstring;
			headstring = "HTTP/1.1 400 Bad Request\r\n\r\n";
			sendTCP(headstring.c_str(), headstring.length());
			closeSocket();
			break;
		}

		if (command.compare("GET") == 0) {
			if (upgrade.find("websocket") != string::npos) {
				string response =
						"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: upgrade\r\nSec-WebSocket-Accept: ";
				response += WebSocketConnection::makeSHA1Hash(key + WS_GUID);
				response += "\r\n\r\n";
				sendTCP(response.c_str(), response.length());
				gConnMan->newWebSocketConnection(getSockfd(), location);
				break;
			}

			gURLMan->processGET(this);
		}

		bytes.clear();
	}
}

bool HTTPConnection::processHeader(ByteString& bytes) {
	int pos = bytes.findSequence("\r\n\r\n", 4);
	char* headerArray = bytes.subString(0, pos).toNewArray();
	string header(headerArray, pos);
	delete[] headerArray;

	if (header.find("\r\n") == 0)
		header = header.substr(2);
	pos = header.find("\r\n");
	string commandLine = header.substr(0, pos);

	int endLoc = commandLine.find(" HTTP/1.1");
	if (endLoc == string::npos)
		return false;
	int startLoc = commandLine.find(" ") + 1;
	if (startLoc >= endLoc)
		return false;
	command = commandLine.substr(0, startLoc);
	location = commandLine.substr(startLoc, endLoc - startLoc);

	trimWhite(command);
	trimWhite(location);

	fillOptionalField(header, accepts, "accept");
	accepts = lowerCasify(accepts);
	fillOptionalField(header, encoding, "accept-encoding");
	encoding = lowerCasify(encoding);
	fillOptionalField(header, connection, "connection");
	connection = lowerCasify(connection);
	if (connection.compare("") == 0)
		connection = "keep-alive";
	fillOptionalField(header, upgrade, "upgrade");
	upgrade = lowerCasify(upgrade);
	fillOptionalField(header, auth, "authorization");
	fillOptionalField(header, length, "content-length");
	fillOptionalField(header, key, "sec-websocket-key");
	fillOptionalField(header, WSVersion, "sec-websocket-version");

	return true;
}

void HTTPConnection::fillOptionalField(string& header, string& field, string fieldName) {
	fieldName += ": ";
	int pos = lowerCasify(header).find(fieldName);
	if (pos != string::npos && header.find("\r\n", pos - 2) == pos - 2) {
		pos += fieldName.length();
		int tpos = header.find("\r\n", pos);
		if (tpos == string::npos)
			tpos = header.length();
		field = header.substr(pos, tpos - pos);
	} else
		field = "";
	trimWhite(field);
}

void HTTPConnection::trimWhite(string& s) {
	while (s[0] == ' ' || s[0] == '\r' || s[0] == '\n')
		s.erase(0, 1);
	while (s[s.length() - 1] == ' ' || s[s.length() - 1] == '\r' || s[s.length() - 1] == '\n')
		s.erase(s.length() - 1, 1);
}

string HTTPConnection::lowerCasify(string s) {
	for (int i = 0; i < s.length(); i++) {
		if (s.at(i) > 0x40 && s.at(i) < 0x5b) {
			s.replace(i, 1, 1, s.at(i) + 0x20);
		}
	}
	return s;
}

string HTTPConnection::getMIME(unsigned short flags) {
	switch (flags & HTTP_MIME_MASK) {
	case HTTP_MIME_HTML:
		return "text/html";
	case HTTP_MIME_PNG:
		return "image/png";
	case HTTP_MIME_JPEG:
		return "image/jpeg";
	case HTTP_MIME_GIF:
		return "image/gif";
	case HTTP_MIME_CSS:
		return "text/css";
	case HTTP_MIME_DOWNLOAD:
		return "application/download";
	case HTTP_MIME_JAVASCRIPT:
		return "application/javascript";
	case HTTP_MIME_PLAIN:
		return "text/plain";
	default:
		return "";
	}
}

string HTTPConnection::getEncod(unsigned short flags) {
	switch (flags & HTTP_ENCOD_MASK) {
	case HTTP_ENCOD_BINARY:
		return "binary";
	default:
		return "";
	}
}
